前幾天一路從:
看流程
↓
拆步驟
↓
找資料放在哪裡
走到現在,終於可以把同一個概念搬進 React。
如果以前使用原生 JavaScript,我們可能會直接建立元素:
const li = document.createElement("li");
li.textContent = text;
memoList.appendChild(li);
這種寫法很直覺:
我要讓畫面多一筆資料,所以我直接去改畫面。
但到了 React,常看到的卻是:
const [memos, setMemos] = useState<string[]>([]);
然後畫面:
{memos.map((memo, index) => (
<li key={`${memo}-${index}`}>
{memo}
</li>
))}
一開始我最不懂的其實不是 useState 怎麼寫。
而是:
我明明只是想多一個
<li>,為什麼 React 不叫我直接建立它?
先看熟悉的版本:
addBtn.addEventListener("click", () => {
const text = memoInput.value.trim();
if (!text) return;
const li = document.createElement("li");
li.textContent = text;
memoList.appendChild(li);
});
流程可以翻成人話:
使用者點 +
↓
取得輸入內容
↓
建立 li
↓
把文字放進 li
↓
把 li 加進畫面
這裡的思考方式比較像:
我要怎麼修改現在的 DOM?
到了 React,會換一個思考方式。
先準備資料:
const [memos, setMemos] = useState<string[]>([]);
先不要急著背 useState。
可以先理解成:
memos
→ 現在的備忘錄資料
setMemos
→ 更新 memos 的方法
一開始:
[]
新增「買牛奶」之後:
["買牛奶"]
React 再根據這份資料決定畫面要顯示什麼。
所以流程變成:
使用者操作
↓
更新 State
↓
State 改變
↓
React 重新 Render
↓
UI 跟著改變
這裡是我覺得最重要的差異:
原生 JavaScript
→ 我直接改畫面
React
→ 我先改資料
→ React 再根據資料更新畫面
先看完整一點的簡化範例:
import { useState } from "react";
export default function Memo() {
const [text, setText] = useState("");
const [memos, setMemos] = useState<string[]>([]);
const handleAdd = () => {
const value = text.trim();
if (!value) return;
setMemos(prev => [...prev, value]);
setText("");
};
return (
<>
<input
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={handleAdd}>
+
</button>
<ul>
{memos.map((memo, index) => (
<li key={`${memo}-${index}`}>
{memo}
</li>
))}
</ul>
</>
);
}
第一次看到這段,如果又從第一行開始硬讀,很容易再次迷路。
所以還是用前幾天的方法:
使用者輸入「買牛奶」
↓
onChange
↓
setText()
↓
text 更新
使用者按下 +
↓
onClick
↓
handleAdd()
↓
取得 text
↓
setMemos()
↓
memos 更新
↓
React Render
↓
畫面出現「買牛奶」
這樣整段就開始有一條線可以追。
onClick 是誰在負責點擊?以前可能寫:
addBtn.addEventListener("click", handleAdd);
到了 React:
<button onClick={handleAdd}>
+
</button>
可以先簡單對照:
JavaScript
addEventListener("click", handleAdd)
↓
React
onClick={handleAdd}
同樣地:
<input
onChange={e => setText(e.target.value)}
/>
就是:
輸入內容改變時,執行這段處理。
所以現在先記:
點擊
→ onClick
輸入改變
→ onChange
這裡還不需要把 useEffect 拉進來。
因為一般的按鈕點擊,不是靠 useEffect 處理。
setMemos() 又在做什麼?這一行:
setMemos(prev => [...prev, value]);
可以拆開看。
假設目前:
prev = ["買牛奶"];
這次新增:
value = "寫文章";
這段:
[...prev, value]
會變成:
["買牛奶", "寫文章"]
所以:
setMemos(prev => [...prev, value]);
可以先理解成:
把原本的資料保留下來,再加入新的資料,最後更新 memos。
流程:
舊資料
["買牛奶"]
↓ 加入新資料
["買牛奶", "寫文章"]
↓ setMemos()
State 更新
map() 是怎麼讓資料變成畫面的?假設:
memos = ["買牛奶", "寫文章"];
畫面需要的是:
<li>買牛奶</li>
<li>寫文章</li>
所以:
memos.map((memo, index) => (
<li key={`${memo}-${index}`}>
{memo}
</li>
))
可以理解成:
["買牛奶", "寫文章"]
↓
一筆一筆轉換
買牛奶
→ <li>買牛奶</li>
寫文章
→ <li>寫文章</li>
所以在這裡,map() 不只是「跑迴圈」。
更像是:
把 Array 裡的資料,轉換成 React 要 Render 的內容。
key這個範例裡:
key={`${memo}-${index}`}
只是為了讓教學先聚焦在 State → Render。
真實專案如果資料本身有穩定且唯一的 ID,會優先使用:
<li key={memo.id}>
而不是依賴 index。
這個之後遇到列表問題時再單獨拆。
到了實際的 React 專案,畫面可能不是備忘錄,而是一個 Table:
API
↓
Response
↓
State
↓
Table dataSource
↓
React Render
↓
畫面
所以如果畫面資料不對,我現在會開始往回找:
這個 Table 現在吃哪份資料?
↓
那份資料是哪個 State?
↓
State 是誰更新的?
↓
是 Props 傳進來的?
還是 API Response?
這其實還是前幾天一直在做的同一件事:
追資料從哪裡來,又往哪裡去。
useState 格式如果只背:
const [memos, setMemos] = useState([]);
很容易過幾天又忘記。
但如果記住:
State
↓
畫面依賴的資料
使用者操作
↓
事件發生
↓
更新 State
↓
React Render
↓
UI 跟著資料變化
之後再看到:
useState
setMemos
onClick
onChange
map
它們就不再是五個分開的咒語。
而是同一條流程裡,不同位置的工具。
下一篇就接一個 React 裡非常常見,也很容易混亂的問題:
State 跟 Props 到底差在哪?這份資料到底是誰的?